Desafio Data Scientist - Mateus Mendelson

Este notebook foi desenvolvido como parte do teste técnico para a Neoway para a vaga de Cientista de Dados Sênior. Este material foi disponibilizado de forma pública sob autorização da empresa.

Para realizar a instalação dos pacotes necessários, basta executar o arquivo requirements.txt.

$ pip install -r requirements.txt

Note que o comando completo se encontra no arquivo Product Data Science.pdf

Primeiro, vamos abrir os arquivos de dataset para analisar as informações que temos.

In [1]:
import pandas as pd
import numpy as np
In [2]:
DATA_FOLDER = 'data/'

# Note that some columns are supposed to be of type int, but they
# are casted to float due to missing values (that are not supported
# in the int type).
ind_df = pd.read_csv(f'{DATA_FOLDER}individuos_espec.csv', sep=';')
con_df = pd.read_csv(f'{DATA_FOLDER}conexoes_espec.csv', sep=';')
In [3]:
print(ind_df.shape)
ind_df.head()
(1000000, 9)
Out[3]:
name idade estado_civil qt_filhos estuda trabalha pratica_esportes transporte_mais_utilizado IMC
0 1 44.0 divorciado 1.0 1.0 0.0 1.0 publico 22.200956
1 2 24.0 casado 0.0 0.0 0.0 1.0 publico 25.378720
2 3 35.0 solteiro 1.0 0.0 0.0 1.0 particular 19.952393
3 4 50.0 casado 1.0 1.0 1.0 0.0 publico 26.732053
4 5 30.0 solteiro 2.0 1.0 0.0 1.0 publico 15.295668

Vamos checar a quantidade de valores vazios em cada coluna para os dois datasets.

In [4]:
ind_df.isna().sum()
Out[4]:
name                              0
idade                         95937
estado_civil                  50073
qt_filhos                     28867
estuda                        40130
trabalha                       6353
pratica_esportes             149124
transporte_mais_utilizado     43033
IMC                          113870
dtype: int64
In [5]:
print(con_df.shape)
con_df.head()
(999999, 5)
Out[5]:
V1 V2 grau proximidade prob_V1_V2
0 1 2 trabalho visita_frequente 0.589462
1 1 3 trabalho visita_rara 0.708465
2 2 4 trabalho visita_casual NaN
3 2 5 trabalho visita_rara 0.638842
4 3 6 amigos mora_junto NaN
In [6]:
con_df.isna().sum()
Out[6]:
V1                  0
V2                  0
grau                0
proximidade         0
prob_V1_V2     500000
dtype: int64

Conforme esperado, apenas metade das conexões foram medidas.

Imputando valores ausentes

Utilizaremos o método KNN para realizar a imputação dos valores ausentes no dataset de indivíduos. Para isso, apenas as linhas sem nenhum valor ausente serão utilizadas para treinamento do algoritmo.

A classe DataImputer realizará todo esse processo.

In [7]:
from customLib.DataImputer import DataImputer

di = DataImputer(ind_df)
di.impute()
imputed_ind_df = di.get_data()
  0%|                                                                                           | 0/86 [00:00<?, ?it/s]
Performing imputations...
100%|██████████████████████████████████████████████████████████████████████████████████| 86/86 [06:52<00:00,  4.80s/it]
In [8]:
imputed_ind_df.isna().sum()
Out[8]:
name                         0
idade                        0
estado_civil                 0
qt_filhos                    0
estuda                       0
trabalha                     0
pratica_esportes             0
transporte_mais_utilizado    0
IMC                          0
dtype: int64
In [9]:
imputed_ind_df.tail()
Out[9]:
name idade estado_civil qt_filhos estuda trabalha pratica_esportes transporte_mais_utilizado IMC
70554 70555 34 casado 1 1 1 1 publico 28.938620
53842 53843 31 casado 2 1 0 0 publico 17.505580
680836 680837 15 solteiro 0 1 0 0 publico 17.623402
405527 405528 27 divorciado 1 0 0 1 publico 21.562605
914631 914632 20 divorciado 0 0 0 1 publico 24.125077

Preparando as features para o treinamento

Utilizaremos a classe FeatureMaker para montar os dados de acordo com os formatos esperados para o treinamento do modelo de predição das conexões.

Caso deseje realizar a imputação dos dados, execute a linha fm = FeatureMaker(imputed_ind_df, con_df). Caso deseje utilizar os dados já processados (que estão salvos em um arquivo .csv junto com este código), execute apenas a linha fm = FeatureMaker(load_from_file=True).

In [7]:
from customLib.FeatureMaker import FeatureMaker

# fm = FeatureMaker(imputed_ind_df, con_df)
fm = FeatureMaker(load_from_file=True)
fm.prepare_sets()

Utilizaremos uma rede neural simples com uma única camada oculta e uma camada de saída. A função de ativação da camada oculta será a ReLU, pois o treinamento com esta função tende a ser mais rápido (uma vez que sua derivada é uma constante). Por outro lado, a função sigmoid será utilizada na camada de saída, pois seus valores variam de 0 até 1 (que são os valores que precisamos como saída).

Uma outra boa prática a ser utilizada é a quantidade de neurônios na camada oculta. Ao longo das minhas experiências, adotar uma quantidade de neurônios nessa camada igual a raiz da quantidade de entradas da camada anterior é um valor bom.

Dado mais tempo, eu faria um treinamento utilizando k-fold e faria uma busca pela quantidade ideal de neurônios na camada oculta, além de testar a adição de mais uma camada oculta (mais do que duas camadas ocultas costuma gerar um aumento computacional que não resulta em grandes melhorias preditivas).

In [8]:
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import torch
import math
import matplotlib.pyplot as plt

class NeuralNetwork(nn.Module):
    def __init__(self, input_dim):
        super(NeuralNetwork, self).__init__()

        self.fc1 = nn.Linear(input_dim, int(math.sqrt(input_dim)))
        self.fc2 = nn.Linear(int(math.sqrt(input_dim)), 1)
        
    def forward(self, x):
        x = F.relu(self.fc1(x))
        
        x = torch.sigmoid(self.fc2(x))
        
        return x

input_dim = 33
model = NeuralNetwork(input_dim)
# checking for gpu
# device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# print(f'===== Using {device} =====')
# if torch.cuda.is_available():
#     print(f'GPU in use: {torch.cuda.get_device_name(0)}')
# model = model.to(device)

model
Out[8]:
NeuralNetwork(
  (fc1): Linear(in_features=33, out_features=5, bias=True)
  (fc2): Linear(in_features=5, out_features=1, bias=True)
)

Por se tratar de um problema de regressão, adotaremos o Mean Squared Error (MSE) como nossa métrica chave.

É importante citar que o Learning Rate chegou a ser testado com valores menores, sendo o valor 0.01 o que resultou em melhor convergência.

Por fim, o treinamento foi feito com apenas 500 épocas. Note que o código implementa o Early Stopping, mas que não chegou a ser alcançado com os parâmetros utilizados. Foram adotadas 500 épocas para que o treinamento não demorasse tanto. O ideal seria utilizar um valor bem alto de épocas e deixar o treinamento prosseguir até que o Early Stopping entrasse em ação.

In [9]:
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)
In [13]:
def train(model, n_epochs, optimizer, criterion, train_loader, valid_loader):
    model.double()
    
    # initialize tracker for minimum validation loss
    valid_loss_min = np.Inf
    
    # initialize losses tracking variables
    train_losses = []
    valid_losses = []
    
    # early stopping parameter
    stop_after = 3
    trials = 0
    
    for epoch in range(1, n_epochs+1):
        # initialize variables to monitor training and validation loss
        train_loss = 0.0
        train_counter = 0
        valid_loss = 0.0
        valid_counter = 0
        
        ###################
        # train the model #
        ###################
        model.train()
        for features_batch, targets_batch in train_loader:
#             features_batch = features_batch.to(device)
#             features_batch = targets_batch.to(device)
            
            outputs_batch = torch.squeeze(model(features_batch))
            
            optimizer.zero_grad()
            # calculate the batch loss
            loss = criterion(outputs_batch, targets_batch)
            # backward pass: compute gradient of the loss with respect to model parameters
            loss.backward()
            # perform a single optimization step (parameter update)
            optimizer.step()
            # update training loss
            train_loss += loss
            train_counter += 1
        train_loss /= train_counter
        train_losses.append(train_loss)
        
        ######################
        # validate the model #
        ######################
        model.eval()
        for features_batch, targets_batch in valid_loader:
#             features_batch = features_batch.to(device)
#             features_batch = targets_batch.to(device)
            
            outputs_batch = torch.squeeze(model(features_batch))
            
            # calculate the batch loss
            loss = criterion(outputs_batch, targets_batch)
            valid_loss += loss
            valid_counter += 1
        valid_loss /= valid_counter
        valid_losses.append(valid_loss)
        
        ## save the model if validation loss has decreased
        if valid_loss < valid_loss_min:
            # print training/validation statistics
            print('Epoch: {} \tTraining Loss: {:.6f} \tValidation Loss: {:.6f}\tSaving model...............'.format(
                epoch, 
                train_loss,
                valid_loss
                ), end='\r')
            valid_loss_min = valid_loss
            torch.save(model.state_dict(), 'best_model.pickle')
            trials = 0
        else:
            trials += 1
            # print training/validation statistics
            print('Epoch: {} \tTraining Loss: {:.6f} \tValidation Loss: {:.6f} \tEarly stopping status: {}/{}'.format(
                epoch, 
                train_loss,
                valid_loss,
                trials, stop_after
                ), end='\r')
            valid_loss_min = valid_loss
            torch.save(model.state_dict(), 'best_model.pickle')
            
            # checking for early stopping
            if trials >= stop_after:
                print(f'Early stopping after {trials} attempts without improvement on the validation set!')
        
    return train_losses, valid_losses

train_losses, valid_losses = train(model, 500, optimizer, criterion, fm.get_loader('train'), fm.get_loader('valid'))
Epoch: 500 	Training Loss: 0.003964 	Validation Loss: 0.004011	Saving model...............
In [30]:
# plot losses
plt.figure(figsize=(20, 10))
plt.plot(list(range(1, len(train_losses)+1)), train_losses, label='Training loss')
plt.plot(list(range(1, len(valid_losses)+1)), valid_losses, label='Valid loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.title('Training and validation losses')
plt.legend()
plt.show()
In [12]:
# Load best model
model.load_state_dict(torch.load('best_model.pickle'))
model.eval()
model.double()
Out[12]:
NeuralNetwork(
  (fc1): Linear(in_features=33, out_features=5, bias=True)
  (fc2): Linear(in_features=5, out_features=1, bias=True)
)
In [14]:
def test(model, criterion, test_loader):
    test_loss = 0
    test_counter = 0
    
    for features_batch, targets_batch in test_loader:
        outputs_batch = torch.squeeze(model(features_batch))

        # calculate the batch loss
        loss = criterion(outputs_batch, targets_batch)
        test_loss += loss
        test_counter += 1
        
    test_loss /= test_counter
    
    return test_loss

test_loss = test(model, criterion, fm.get_loader('test'))

print(f'Loss média no conjunto de testes: {test_loss}')
Loss média no conjunto de testes: 0.004125380131558439

Preenchimento das taxas de transmissão ausentes

In [15]:
predict_loader = fm.get_loader('predict')
In [16]:
from tqdm import tqdm

def predict(model, predict_loader):
    model.double()
    model.eval()
    predictions = []
    
    for features_batch, _ in tqdm(predict_loader):
        outputs_batch = torch.squeeze(model(features_batch))
        
        for predicted in outputs_batch:
            predictions.append(predicted.item())
            
    return predictions

predictions = predict(model, predict_loader)
100%|████████████████████████████████████████████████████████████████████████████████| 500/500 [00:08<00:00, 61.40it/s]
In [17]:
features, targets = fm.fill_nan_values(predictions)
In [18]:
# cols = fm.get_columns().copy()
# cols.append('prob_V1_V2')
cols = ['V1_idade', 'V1_qt_filhos', 'V1_estuda', 'V1_trabalha', 'V1_pratica_esportes', 'V1_IMC', 'V1_estado_civil-casado', 'V1_estado_civil-divorciado', 'V1_estado_civil-solteiro', 'V1_estado_civil-viuvo', 'V1_transporte_mais_utilizado-particular', 'V1_transporte_mais_utilizado-publico', 'V1_transporte_mais_utilizado-taxi', 'V2_idade', 'V2_qt_filhos', 'V2_estuda', 'V2_trabalha', 'V2_pratica_esportes', 'V2_IMC', 'V2_estado_civil-casado', 'V2_estado_civil-divorciado', 'V2_estado_civil-solteiro', 'V2_estado_civil-viuvo', 'V2_transporte_mais_utilizado-particular', 'V2_transporte_mais_utilizado-publico', 'V2_transporte_mais_utilizado-taxi', 'grau-amigos', 'grau-familia', 'grau-trabalho', 'proximidade-mora_junto', 'proximidade-visita_casual', 'proximidade-visita_frequente', 'proximidade-visita_rara', 'prob_V1_V2']
norm_params = fm.get_norm_params()
In [19]:
from tqdm import tqdm

data = features.tolist()

# add probabilities to the list
for idx in tqdm(range(len(data))):
    data[idx].append(targets[idx])
100%|█████████████████████████████████████████████████████████████████████| 999999/999999 [00:00<00:00, 1347675.32it/s]
In [20]:
full_df = pd.DataFrame(data, columns=cols)
In [21]:
pd.set_option('display.max_columns', None)
full_df.head()
Out[21]:
V1_idade V1_qt_filhos V1_estuda V1_trabalha V1_pratica_esportes V1_IMC V1_estado_civil-casado V1_estado_civil-divorciado V1_estado_civil-solteiro V1_estado_civil-viuvo V1_transporte_mais_utilizado-particular V1_transporte_mais_utilizado-publico V1_transporte_mais_utilizado-taxi V2_idade V2_qt_filhos V2_estuda V2_trabalha V2_pratica_esportes V2_IMC V2_estado_civil-casado V2_estado_civil-divorciado V2_estado_civil-solteiro V2_estado_civil-viuvo V2_transporte_mais_utilizado-particular V2_transporte_mais_utilizado-publico V2_transporte_mais_utilizado-taxi grau-amigos grau-familia grau-trabalho proximidade-mora_junto proximidade-visita_casual proximidade-visita_frequente proximidade-visita_rara prob_V1_V2
0 0.354839 0.111111 1.0 0.0 1.0 0.205777 0.0 1.0 0.0 0.0 0.0 1.0 0.0 0.193548 0.000000 0.0 0.0 1.0 0.243471 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.589462
1 0.354839 0.111111 1.0 0.0 1.0 0.205777 0.0 1.0 0.0 0.0 0.0 1.0 0.0 0.282258 0.111111 0.0 0.0 1.0 0.179106 0.0 0.0 1.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0 0.708465
2 0.193548 0.000000 0.0 0.0 1.0 0.243471 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.403226 0.111111 1.0 1.0 0.0 0.259523 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0 0.0 1.0 0.0 0.0 0.634575
3 0.193548 0.000000 0.0 0.0 1.0 0.243471 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.241935 0.222222 1.0 0.0 1.0 0.123869 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0 0.638842
4 0.282258 0.111111 0.0 0.0 1.0 0.179106 0.0 0.0 1.0 0.0 1.0 0.0 0.0 0.161290 0.111111 0.0 1.0 0.0 0.184568 1.0 0.0 0.0 0.0 0.0 1.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 0.0 0.347278

Revertendo one-hot-encoding e normalização

Nosso objetivo agora é obter os dados nos formatos e intervalos originais.

In [22]:
estado_civil = ['casado', 'divorciado', 'solteiro', 'viuvo']
transporte_mais_utilizado = ['particular', 'publico', 'taxi']
grau = ['amigos', 'familia', 'trabalho']
proximidade = ['mora_junto', 'visita_casual', 'visita_frequente', 'visita_rara']
In [23]:
def get_categorical(elements_list, categorical_values):
    values = []
    for element in elements_list:
        values.append(categorical_values[element.index(1.0)])
        
    return values
In [24]:
# estado civil
print('Processing "estado_civil"...')

cols = ['V1_estado_civil-casado', 'V1_estado_civil-divorciado', 'V1_estado_civil-solteiro', 'V1_estado_civil-viuvo']
temp_data = full_df[cols].values.tolist()
full_df.drop(columns=cols, inplace=True)
col_data = get_categorical(temp_data, estado_civil)
full_df['V1_estado_civil'] = col_data

cols = ['V2_estado_civil-casado', 'V2_estado_civil-divorciado', 'V2_estado_civil-solteiro', 'V2_estado_civil-viuvo']
temp_data = full_df[cols].values.tolist()
full_df.drop(columns=cols, inplace=True)
col_data = get_categorical(temp_data, estado_civil)
full_df['V2_estado_civil'] = col_data

# transporte mais utilizado
print('Processing "transporte_mais_utilizado"...')

cols = ['V1_transporte_mais_utilizado-particular', 'V1_transporte_mais_utilizado-publico', 'V1_transporte_mais_utilizado-taxi']
temp_data = full_df[cols].values.tolist()
full_df.drop(columns=cols, inplace=True)
col_data = get_categorical(temp_data, transporte_mais_utilizado)
full_df['V1_transporte_mais_utilizado'] = col_data

cols = ['V2_transporte_mais_utilizado-particular', 'V2_transporte_mais_utilizado-publico', 'V2_transporte_mais_utilizado-taxi']
temp_data = full_df[cols].values.tolist()
full_df.drop(columns=cols, inplace=True)
col_data = get_categorical(temp_data, transporte_mais_utilizado)
full_df['V2_transporte_mais_utilizado'] = col_data

# grau
print('Processing "grau"...')

cols = ['grau-amigos', 'grau-familia', 'grau-trabalho']
temp_data = full_df[cols].values.tolist()
full_df.drop(columns=cols, inplace=True)
col_data = get_categorical(temp_data, grau)
full_df['grau'] = col_data

# proximidade
print('Processing "proximidade"...')

cols = ['proximidade-mora_junto', 'proximidade-visita_casual', 'proximidade-visita_frequente', 'proximidade-visita_rara']
temp_data = full_df[cols].values.tolist()
full_df.drop(columns=cols, inplace=True)
col_data = get_categorical(temp_data, proximidade)
full_df['proximidade'] = col_data
Processing "estado_civil"...
Processing "transporte_mais_utilizado"...
Processing "grau"...
Processing "proximidade"...
In [25]:
full_df.head()
Out[25]:
V1_idade V1_qt_filhos V1_estuda V1_trabalha V1_pratica_esportes V1_IMC V2_idade V2_qt_filhos V2_estuda V2_trabalha V2_pratica_esportes V2_IMC prob_V1_V2 V1_estado_civil V2_estado_civil V1_transporte_mais_utilizado V2_transporte_mais_utilizado grau proximidade
0 0.354839 0.111111 1.0 0.0 1.0 0.205777 0.193548 0.000000 0.0 0.0 1.0 0.243471 0.589462 divorciado casado publico publico trabalho visita_frequente
1 0.354839 0.111111 1.0 0.0 1.0 0.205777 0.282258 0.111111 0.0 0.0 1.0 0.179106 0.708465 divorciado solteiro publico particular trabalho visita_rara
2 0.193548 0.000000 0.0 0.0 1.0 0.243471 0.403226 0.111111 1.0 1.0 0.0 0.259523 0.634575 casado casado publico publico trabalho visita_casual
3 0.193548 0.000000 0.0 0.0 1.0 0.243471 0.241935 0.222222 1.0 0.0 1.0 0.123869 0.638842 casado solteiro publico publico trabalho visita_rara
4 0.282258 0.111111 0.0 0.0 1.0 0.179106 0.161290 0.111111 0.0 1.0 0.0 0.184568 0.347278 solteiro casado particular publico amigos mora_junto
In [26]:
def denormalize(df, col, norm_params):
    maxim = norm_params[f'max_{col}']
    minim = norm_params[f'min_{col}']
    
    v_col = f'V1_{col}'
    df[v_col] = (df[v_col]*(maxim - minim)) + minim
    
    v_col = f'V2_{col}'
    df[v_col] = (df[v_col]*(maxim - minim)) + minim

cols = ['idade', 'qt_filhos', 'IMC']
for col in tqdm(cols):
    denormalize(full_df, col, norm_params)
100%|████████████████████████████████████████████████████████████████████████████████████| 3/3 [00:00<00:00, 16.48it/s]
In [27]:
full_df.to_csv(f'{DATA_FOLDER}conexoes.csv', index=False)
In [28]:
full_df = pd.read_csv(f'{DATA_FOLDER}conexoes.csv')

full_df.head()
Out[28]:
V1_idade V1_qt_filhos V1_estuda V1_trabalha V1_pratica_esportes V1_IMC V2_idade V2_qt_filhos V2_estuda V2_trabalha V2_pratica_esportes V2_IMC prob_V1_V2 V1_estado_civil V2_estado_civil V1_transporte_mais_utilizado V2_transporte_mais_utilizado grau proximidade
0 44.0 1.0 1.0 0.0 1.0 22.200956 24.0 0.0 0.0 0.0 1.0 25.378720 0.589462 divorciado casado publico publico trabalho visita_frequente
1 44.0 1.0 1.0 0.0 1.0 22.200956 35.0 1.0 0.0 0.0 1.0 19.952393 0.708465 divorciado solteiro publico particular trabalho visita_rara
2 24.0 0.0 0.0 0.0 1.0 25.378720 50.0 1.0 1.0 1.0 0.0 26.732053 0.634575 casado casado publico publico trabalho visita_casual
3 24.0 0.0 0.0 0.0 1.0 25.378720 30.0 2.0 1.0 0.0 1.0 15.295668 0.638842 casado solteiro publico publico trabalho visita_rara
4 35.0 1.0 0.0 0.0 1.0 19.952393 20.0 1.0 0.0 1.0 0.0 20.412942 0.347278 solteiro casado particular publico amigos mora_junto

Análise exploratória

Finalmente, vamos realizar algumas análises em busca de possíveis padrões.

In [29]:
import plotly.express as px

fig = px.histogram(full_df, x='prob_V1_V2')
fig.show()
In [30]:
import matplotlib.pyplot as plt
import scipy.stats as stats

fig, ax = plt.subplots(figsize=(8, 8))
stats.probplot(full_df['prob_V1_V2'], plot=ax);

A distribuição das taxas de contaminação se aproxima de uma normal.

In [31]:
mean = full_df['prob_V1_V2'].mean()
std = full_df['prob_V1_V2'].std()

print(f'Média das probabilidades de transmissão: {mean}')
print(f'Desvio padrão das probabilidades de transmissão: {std}')
Média das probabilidades de transmissão: 0.4850645529844639
Desvio padrão das probabilidades de transmissão: 0.16816967171967273
In [32]:
full_df.head()
Out[32]:
V1_idade V1_qt_filhos V1_estuda V1_trabalha V1_pratica_esportes V1_IMC V2_idade V2_qt_filhos V2_estuda V2_trabalha V2_pratica_esportes V2_IMC prob_V1_V2 V1_estado_civil V2_estado_civil V1_transporte_mais_utilizado V2_transporte_mais_utilizado grau proximidade
0 44.0 1.0 1.0 0.0 1.0 22.200956 24.0 0.0 0.0 0.0 1.0 25.378720 0.589462 divorciado casado publico publico trabalho visita_frequente
1 44.0 1.0 1.0 0.0 1.0 22.200956 35.0 1.0 0.0 0.0 1.0 19.952393 0.708465 divorciado solteiro publico particular trabalho visita_rara
2 24.0 0.0 0.0 0.0 1.0 25.378720 50.0 1.0 1.0 1.0 0.0 26.732053 0.634575 casado casado publico publico trabalho visita_casual
3 24.0 0.0 0.0 0.0 1.0 25.378720 30.0 2.0 1.0 0.0 1.0 15.295668 0.638842 casado solteiro publico publico trabalho visita_rara
4 35.0 1.0 0.0 0.0 1.0 19.952393 20.0 1.0 0.0 1.0 0.0 20.412942 0.347278 solteiro casado particular publico amigos mora_junto

Análise por faixa etária

Já que muitas doenças costumam ser mais propensas a certas faixas etárias, vamos primeiro checar a taxa média de transmissão para cada faixa etária.

In [33]:
v1_age_df = full_df.groupby(by=['V1_idade']).agg({'prob_V1_V2': 'mean', 'V1_IMC': 'count', 'V1_pratica_esportes': 'mean'}).rename(columns={'prob_V1_V2': 'prob', 'V1_IMC': 'count'})
v1_age_df.reset_index(inplace=True)
v1_age_df.sort_values('prob', ascending=False, inplace=True)

v1_age_df
Out[33]:
V1_idade prob count V1_pratica_esportes
92 92.0 0.715907 2 1.000000
1 1.0 0.590656 12 0.500000
99 106.0 0.584884 2 0.000000
100 110.0 0.578264 2 1.000000
3 3.0 0.571617 58 0.517241
... ... ... ... ...
94 95.0 0.411224 4 0.500000
93 93.0 0.347275 2 0.000000
88 88.0 0.328109 8 0.250000
97 100.0 0.293671 2 1.000000
96 97.0 0.143067 2 0.000000

102 rows × 4 columns

Iremos analisar apenas faixas etárias que ocorrem pelo menos 2000 vezes (valor aproximado da mediana).

In [34]:
print(f'Note que as seguintes idades ficarão fora da análise por terem poucas amostras:')
print(v1_age_df[v1_age_df['count'] < 2000].sort_values('V1_idade')['V1_idade'].astype('int32').unique())
Note que as seguintes idades ficarão fora da análise por terem poucas amostras:
[  0   1   2   3   4   5   6   7  59  60  61  62  63  64  65  66  67  68
  69  70  71  72  73  74  75  76  77  78  79  80  81  82  83  84  85  86
  87  88  89  90  91  92  93  95  96  97 100 103 106 110 111]
In [35]:
v1_age_df = v1_age_df[v1_age_df['count'] >= 2000]

v1_age_df.head()
Out[35]:
V1_idade prob count V1_pratica_esportes
8 8.0 0.552327 2116 0.553875
12 12.0 0.548602 8183 0.590859
14 14.0 0.548567 12898 0.547372
11 11.0 0.548280 6250 0.547200
10 10.0 0.547684 4668 0.550985
In [36]:
fig = px.box(v1_age_df, y='prob', hover_data=['V1_idade'])
fig.show()

As seguintes idades possuem uma taxa média de contaminação muitíssimo alta (outliers): 8, 9, 10, 11, 12, 13, 14 e 15, ou seja, a faixa etária de 8 a 15 anos de idade possui uma altíssima taxa de transmissão da doença.

Vamos analisar as top 10 idades com os maiores potenciais de contaminação.

In [37]:
v1_age_df[:10]
Out[37]:
V1_idade prob count V1_pratica_esportes
8 8.0 0.552327 2116 0.553875
12 12.0 0.548602 8183 0.590859
14 14.0 0.548567 12898 0.547372
11 11.0 0.548280 6250 0.547200
10 10.0 0.547684 4668 0.550985
13 13.0 0.547383 10484 0.558947
15 15.0 0.545990 15732 0.538266
9 9.0 0.542277 3144 0.561705
17 17.0 0.488427 21918 0.552423
18 18.0 0.486603 24584 0.548080

As 10 idades que mais contaminam são de 8 a 15 anos e 17 e 18 anos de idade. Note que as taxas de transmissão a partir da oitava posição já estão muito próximas. Sendo assim, iremos utilizar apenas a faixa etária de 8 a 15 anos de idade como sendo de interesse.

Vamos checar as taxas médias de as pessoas serem contaminadas por faixas etárias.

In [38]:
v2_age_df = full_df.groupby(by=['V2_idade']).agg({'prob_V1_V2': 'mean', 'V2_IMC': 'count', 'V2_pratica_esportes': 'mean'}).rename(columns={'prob_V1_V2': 'prob', 'V2_IMC': 'count'})
v2_age_df.reset_index(inplace=True)
v2_age_df.sort_values('prob', ascending=False, inplace=True)

v2_age_df.head()
Out[38]:
V2_idade prob count V2_pratica_esportes
104 124.0 0.767903 1 1.00
101 106.0 0.678901 1 0.00
93 93.0 0.594659 4 0.25
92 92.0 0.590612 4 0.50
98 98.0 0.576413 1 1.00

Iremos analisar apenas faixas etárias que ocorrem pelo menos 2000 vezes (valor aproximado da mediana).

In [39]:
v2_age_df = v2_age_df[v2_age_df['count'] >= 2000]

v2_age_df.head()
Out[39]:
V2_idade prob count V2_pratica_esportes
9 9.0 0.497263 3198 0.563790
11 11.0 0.493549 6267 0.548109
8 8.0 0.493108 2164 0.549908
10 10.0 0.492334 4632 0.547280
13 13.0 0.490509 10545 0.558369
In [40]:
fig = px.box(v2_age_df, y='prob', hover_data=['V2_idade'])
fig.show()

A idade de 9 anos se destaca como outlier.

Vamos analisar as top 10 idades com as maiores taxas de serem contaminadas.

In [41]:
v2_age_df[:10]
Out[41]:
V2_idade prob count V2_pratica_esportes
9 9.0 0.497263 3198 0.563790
11 11.0 0.493549 6267 0.548109
8 8.0 0.493108 2164 0.549908
10 10.0 0.492334 4632 0.547280
13 13.0 0.490509 10545 0.558369
12 12.0 0.490050 8069 0.583592
15 15.0 0.489697 15411 0.536305
19 19.0 0.489269 26794 0.547025
20 20.0 0.489121 28717 0.546262
16 16.0 0.488703 18909 0.543974

As 10 idades que mais são contaminadas são de 8 a 13, 15, 16, 19 e 20 anos de idade.

De forma geral, a doença se propaga com maior intensidade entre crianças, adolescentes e jovens adultos (nas faixas de 8 a 20 anos de idade).

A seguir, analisaremos as ligações entre pessoas dessas faixas etárias.

In [42]:
v1_ages_of_interest = [8, 9, 10, 11, 12, 13, 14, 15]
v2_ages_of_interest = [8, 9, 10, 11, 12, 13, 15, 16, 19, 20]

ages_df = full_df[full_df['V1_idade'].isin(v1_ages_of_interest) & full_df['V2_idade'].isin(v2_ages_of_interest)]
In [43]:
grau_df = ages_df.groupby(by=['grau']).agg({'prob_V1_V2': 'mean', 'V2_IMC': 'count'}).rename(columns={'prob_V1_V2': 'prob', 'V2_IMC': 'count'})
grau_df.reset_index(inplace=True)
grau_df.sort_values('prob', ascending=False, inplace=True)

grau_df
Out[43]:
grau prob count
0 amigos 0.584043 2614
2 trabalho 0.583530 2683
1 familia 0.499235 2585

Dentre as faixas etárias que mais se contaminam, as contaminações acontecem com maior frequência entre amigos e, logo em seguinda, com quase a mesma probabilidade de contaminação, em ambiente de trabalho. O ambiente familiar é o que apresenta a menor probabilidade de contaminação.

Uma possível recomendação de política de saúde seria a suspensão de interações presenciais fora do ambiente familiar, especialmente (mas não exclusivamente) para pessoas na faixa etária de 8 a 20 anos de idade.

Análise por IMC

O IMC será transformado para faixas, de acordo com a tabela abaixo (adaptada da Wikipedia):

Faixa IMC Situação do peso
Até 18,49 abaixo
De 18,50 até 24,99 normal
De 25 até 29,99 acima
A partir de 30 obesidade
In [44]:
def get_imc_label(value):
    if value <= 18.49:
        return 'abaixo'
    elif value > 18.49 and value <= 24.99:
        return 'normal'
    elif value > 24.99 and value <= 29.99:
        return 'acima'
    else:
        return 'obesidade'

def fill_imc_label(df):
    df['V1_IMC_label'] = df['V1_IMC'].apply(get_imc_label)
    df['V2_IMC_label'] = df['V2_IMC'].apply(get_imc_label)

fill_imc_label(full_df)

full_df.head()
Out[44]:
V1_idade V1_qt_filhos V1_estuda V1_trabalha V1_pratica_esportes V1_IMC V2_idade V2_qt_filhos V2_estuda V2_trabalha V2_pratica_esportes V2_IMC prob_V1_V2 V1_estado_civil V2_estado_civil V1_transporte_mais_utilizado V2_transporte_mais_utilizado grau proximidade V1_IMC_label V2_IMC_label
0 44.0 1.0 1.0 0.0 1.0 22.200956 24.0 0.0 0.0 0.0 1.0 25.378720 0.589462 divorciado casado publico publico trabalho visita_frequente normal acima
1 44.0 1.0 1.0 0.0 1.0 22.200956 35.0 1.0 0.0 0.0 1.0 19.952393 0.708465 divorciado solteiro publico particular trabalho visita_rara normal normal
2 24.0 0.0 0.0 0.0 1.0 25.378720 50.0 1.0 1.0 1.0 0.0 26.732053 0.634575 casado casado publico publico trabalho visita_casual acima acima
3 24.0 0.0 0.0 0.0 1.0 25.378720 30.0 2.0 1.0 0.0 1.0 15.295668 0.638842 casado solteiro publico publico trabalho visita_rara acima abaixo
4 35.0 1.0 0.0 0.0 1.0 19.952393 20.0 1.0 0.0 1.0 0.0 20.412942 0.347278 solteiro casado particular publico amigos mora_junto normal normal
In [45]:
v1_imc_df = full_df.groupby(by=['V1_IMC_label']).agg({'prob_V1_V2': 'mean', 'V1_IMC': 'count'}).rename(columns={'prob_V1_V2': 'prob', 'V1_IMC': 'count'})
v1_imc_df.reset_index(inplace=True)
v1_imc_df.sort_values('prob', ascending=False, inplace=True)

v1_imc_df
Out[45]:
V1_IMC_label prob count
3 obesidade 0.485195 124750
0 abaixo 0.485140 289576
2 normal 0.485035 418585
1 acima 0.484910 167088

É possível notar que quanto menor for o IMC, maior é o potencial de uma pessoa transmitir a doença para outra pessoa.

In [46]:
v2_imc_df = full_df.groupby(by=['V2_IMC_label']).agg({'prob_V1_V2': 'mean', 'V2_IMC': 'count'}).rename(columns={'prob_V1_V2': 'prob', 'V2_IMC': 'count'})
v2_imc_df.reset_index(inplace=True)
v2_imc_df.sort_values('prob', ascending=False, inplace=True)

v2_imc_df
Out[46]:
V2_IMC_label prob count
3 obesidade 0.495433 124985
1 acima 0.488715 167202
2 normal 0.484031 418872
0 abaixo 0.479965 288940

Por outro lado, quanto maior for o IMC de uma pessoa, maior é o potencial de ela ser contaminada.

Entretanto, é importante notar que tanto para contaminadores quanto para contaminados as probabilidades de transmissão são próximas.

Vamos realizar uma análise extra: de correlação entre IMC e a taxa de contaminação.

In [47]:
import seaborn as sn

imc_df = full_df[['V1_IMC', 'V2_IMC', 'prob_V1_V2']]
corr_matrix = imc_df.corr()

sn.heatmap(corr_matrix, annot=True)
plt.show()

Com a análise das correlações acima, é possível concluir que não há correlação entre IMC e a taxa de contaminação, pois os valores das correlações com a coluna prob_V1_V2 são muito próximas de zero.

Análise de transporte mais utilizado

Essa parte da análise avalia os transportes mais utilizados.

In [48]:
v1_transp_df = full_df.groupby(by=['V1_transporte_mais_utilizado']).agg({'prob_V1_V2': 'mean', 'V1_IMC': 'count'}).rename(columns={'prob_V1_V2': 'prob', 'V1_IMC': 'count'})
v1_transp_df.reset_index(inplace=True)
v1_transp_df.sort_values('prob', ascending=False, inplace=True)

v1_transp_df
Out[48]:
V1_transporte_mais_utilizado prob count
1 publico 0.494957 606463
0 particular 0.475872 344468
2 taxi 0.427336 49068

Note que usuários de transporte público possuem uma maior taxa de transmissão da doença.

In [49]:
v2_transp_df = full_df.groupby(by=['V2_transporte_mais_utilizado']).agg({'prob_V1_V2': 'mean', 'V2_IMC': 'count'}).rename(columns={'prob_V1_V2': 'prob', 'V2_IMC': 'count'})
v2_transp_df.reset_index(inplace=True)
v2_transp_df.sort_values('prob', ascending=False, inplace=True)

v2_transp_df
Out[49]:
V2_transporte_mais_utilizado prob count
0 particular 0.550608 343769
2 taxi 0.467534 49404
1 publico 0.449361 606826

Aqui, é interessante notar que usuários de transporte particular possuem uma alta taxa de contração da doença. Isso é curioso, pois espera-se que o transporte público, devido a grande aglomeração de pessoas, tivesse a maior taxa de contaminações (assim como foi no perfil dos transmissores).

Vamos analisar isso mais a fundo.

In [50]:
transp_df = full_df[full_df['V2_transporte_mais_utilizado'] == 'particular']
In [51]:
grau_df = transp_df.groupby(by=['grau']).agg({'prob_V1_V2': 'mean', 'V2_IMC': 'count'}).rename(columns={'prob_V1_V2': 'prob', 'V2_IMC': 'count'})
grau_df.reset_index(inplace=True)
grau_df.sort_values('prob', ascending=False, inplace=True)

grau_df
Out[51]:
grau prob count
0 amigos 0.579308 114638
2 trabalho 0.579217 114434
1 familia 0.493379 114697

O grau da relação entre as pessoas mostrou o mesmo resultado obtido na análise geral de grau (amigos e trabalho com maiores taxas). Essa análise não nos deu muita informação.

Agora, vamos analisar a proximidade.

In [52]:
prox_df = transp_df.groupby(by=['proximidade']).agg({'prob_V1_V2': 'mean', 'V2_IMC': 'count'}).rename(columns={'prob_V1_V2': 'prob', 'V2_IMC': 'count'})
prox_df.reset_index(inplace=True)
prox_df.sort_values('prob', ascending=False, inplace=True)

prox_df
Out[52]:
proximidade prob count
1 visita_casual 0.637340 103006
2 visita_frequente 0.551781 68769
3 visita_rara 0.511259 137493
0 mora_junto 0.446141 34501
In [53]:
df = full_df.groupby(by=['V2_transporte_mais_utilizado', 'proximidade', 'V2_estado_civil']).agg({'prob_V1_V2': 'mean', 'V2_IMC': 'count'}).rename(columns={'prob_V1_V2': 'prob', 'V2_IMC': 'count'})
df.reset_index(inplace=True)
df.sort_values('prob', ascending=False, inplace=True)
df = df[df['count'] >= 2000]

df[:10]
Out[53]:
V2_transporte_mais_utilizado proximidade V2_estado_civil prob count
6 particular visita_casual solteiro 0.638234 46059
4 particular visita_casual casado 0.637436 28747
7 particular visita_casual viuvo 0.636466 9477
5 particular visita_casual divorciado 0.635434 18723
10 particular visita_frequente solteiro 0.553621 30830
38 taxi visita_casual solteiro 0.553083 6690
37 taxi visita_casual divorciado 0.552900 2772
8 particular visita_frequente casado 0.551243 19374
9 particular visita_frequente divorciado 0.549674 12354
36 taxi visita_casual casado 0.549532 4047

Usuários que utilizam transporte particular com visitas casuais costumam se contaminar com mais frequência (independente do estado civil). Isso pode acontecer devido a falsa sensação de segurança que este cenário transmite. O uso de transporte particular diminui o contato entre as pessoas, o que gera uma maior sensação de segurança. Além disso, visitas casuais também podem gerar a mesma sensação (pois costumamos pensar que contatos esporádicos são rápidos e "não dá tempo de contaminar"). Isso resulta em um afrouxamento nas medidas preventivas de contaminação. Note que os 4 perfis com maior taxa de contaminação estão dentro dessas características e com taxas de aquisição da doença acima de 60%.

Aqui, uma possível política de saúde seria a conscientização da população, para que não relaxem as medidas preventivas mesmo em contatos curtos nem em meios de transporte mais isolados de outras pessoas.

Análise da prática de esportes

Agora, iremos analisar se há alguma relação entre as taxas de transmissão e a prática de esportes.

In [54]:
df = full_df.groupby(by=['V1_pratica_esportes', 'V2_pratica_esportes']).agg({'prob_V1_V2': 'mean', 'V2_IMC': 'count'}).rename(columns={'prob_V1_V2': 'prob', 'V2_IMC': 'count'})
df.reset_index(inplace=True)
df.sort_values('prob', ascending=False, inplace=True)
df = df[df['count'] >= 2000]

df
Out[54]:
V1_pratica_esportes V2_pratica_esportes prob count
2 1.0 0.0 0.531713 247816
3 1.0 1.0 0.530981 299651
0 0.0 0.0 0.429781 204850
1 0.0 1.0 0.428564 247682
In [55]:
df = full_df.groupby(by=['V2_pratica_esportes', 'V1_pratica_esportes']).agg({'prob_V1_V2': 'mean', 'V2_IMC': 'count'}).rename(columns={'prob_V1_V2': 'prob', 'V2_IMC': 'count'})
df.reset_index(inplace=True)
df.sort_values('prob', ascending=False, inplace=True)
df = df[df['count'] >= 2000]

df
Out[55]:
V2_pratica_esportes V1_pratica_esportes prob count
1 0.0 1.0 0.531713 247816
3 1.0 1.0 0.530981 299651
0 0.0 0.0 0.429781 204850
2 1.0 0.0 0.428564 247682

Pessoas que praticam esportes possuem uma maior taxa de transmissão da doença, independente de se a pessoa que é contaminada pratica ou não. Isso pode ser um indicativo de que as contaminações não ocorrem nos locais de prática de esportes.

In [56]:
df = full_df.groupby(by=['V1_transporte_mais_utilizado', 'V2_transporte_mais_utilizado']).agg({'prob_V1_V2': 'mean', 'V2_IMC': 'count'}).rename(columns={'prob_V1_V2': 'prob', 'V2_IMC': 'count'})
df.reset_index(inplace=True)
df.sort_values('prob', ascending=False, inplace=True)
df = df[df['count'] >= 2000]

df[:10]
Out[56]:
V1_transporte_mais_utilizado V2_transporte_mais_utilizado prob count
3 publico particular 0.561560 208938
0 particular particular 0.539358 118037
6 taxi particular 0.493432 16794
5 publico taxi 0.477605 29925
2 particular taxi 0.458520 17075
4 publico publico 0.458513 367600
1 particular publico 0.441493 209356
8 taxi taxi 0.406190 2404
7 taxi publico 0.391877 29870

Não parece haver fortes relações entre o tipo de transporte entre as duplas da contaminação. O que também é um indicativo de que as contaminações não ocorrem nos meios de transporte.

In [57]:
df = full_df.groupby(by=['V1_pratica_esportes', 'V1_transporte_mais_utilizado', 'grau']).agg({'prob_V1_V2': 'mean', 'V2_IMC': 'count'}).rename(columns={'prob_V1_V2': 'prob', 'V2_IMC': 'count'})
df.reset_index(inplace=True)
df.sort_values('prob', ascending=False, inplace=True)
df = df[df['count'] >= 2000]

df[:10]
Out[57]:
V1_pratica_esportes V1_transporte_mais_utilizado grau prob count
12 1.0 publico amigos 0.571620 110484
14 1.0 publico trabalho 0.571296 111169
9 1.0 particular amigos 0.547874 62741
11 1.0 particular trabalho 0.547450 62638
15 1.0 taxi amigos 0.502274 8714
17 1.0 taxi trabalho 0.498624 8834
13 1.0 publico familia 0.482794 111434
10 1.0 particular familia 0.466555 62767
3 0.0 publico amigos 0.464354 91259
5 0.0 publico trabalho 0.463467 90824

Aqui, é possível notar que aqueles que praticam atividades físicas contaminam os outros com mais frequência independente do meio de transporte que utilizam (note que os 3 meios de transporte aparecem no topo). Entratanto, é importante notar, mais uma vez, que interações entre amigos e colegas de trabalho resultam em maiores taxas de contaminação.

Esses dados reforçam a sugestão da política pública de isolamento social.

É necessária a coleta de mais dados e de um trabalho interdisciplinar para analisar qual a relação entra as práticas esportivas e a propagação do vírus. Maiores temperaturas corporais são favoráveis ao vírus? Atividades físicas extenuantes causam uma baixa temporária da imunidade da pessoa. Seria isso mais uma possível causa para essas altas taxas?